package com.bren.android.androidproject;

import java.util.List;

import android.app.AlarmManager;
import android.app.IntentService;
import android.app.PendingIntent;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.preference.PreferenceManager;

import com.google.android.maps.GeoPoint;


public class MapService extends IntentService {
	
	//provider must be GPS_PROVIDER for mock data to work
	
	// 1. Get Position(store as currentBest in DB)
	//	1.2 If accuracy ok, compare to points in DB
	//		1.3 if near increase rate of location checking(calculate distance to nearest point)
	//			1.4 if in zone,Unless manual override on, turn off ringer.(Store flag in DB if we switch off based on zone)
	//		2.1 If out of zone turn ringer back on(check matrix maybe timer or schedule will hold off)
	
	// Must keep thread running to get callbacks? setIntentRedeliver(true)
	
	// Service flags
	private static final int OUT_OF_SERVICE=0;
	private static final int TEMPORARILY_UNAVAILABLE=1;
	private static final int AVAILABLE=2;
	
	private static final int ONE_MINUTE = 1000 * 60 * 1;
	
	//thread variables
	private boolean shouldRun=true;
	
	// Location variables
	private LocationManager locationManager;
	private String provider;
	
	private Location bestEstimate;// current best location(could be an estimate from 2 location providers)
	private Location newLocation;// newly reported location
	private Location oldLocation=null;// last but one location
	private float currentSpeed;// meters per second
	
	private boolean isListening;// used to switch service to sleep when not needed
	private int nonWindowCountdown;// counts nonListeningWindow in minute timer countdown
		
	// DB variables
	private SQLiteDatabase db;
	private ContentValues contentValues = new ContentValues(3);//(ptNumber,?,?)
	private Cursor result=null;
	private int radius;
	
	// Setup location listener
	private LocationListener locationListener = new LocationListener() {
		
		public void onLocationChanged(Location location) {
			android.util.Log.w("MapService.LocationListener","location changed");
			newLocation = location;
			
			if(isBetterLocation(newLocation,bestEstimate)){
				bestEstimate = newLocation;
			}
			
			// check location manager, provider and callbacks ok
			// if provider change unregister and reregister callbacks
			
			// check age
			// check accuracy
			// mock data use GPS provider
			// Eclipse GPX file for mock route
			
			// if mapping disabled do nothing
			SharedPreferences prefs=PreferenceManager.getDefaultSharedPreferences(MapService.this);
	    	if(new Boolean(prefs.getBoolean("mapstatus",true))) {
	    		
	    		android.util.Log.w("MapService","Map service is on");
	    		// but would need to switch back on the ringer if map service had previously switched off
	    		// check our db matrix for any live ringer shutdown and reverse
	    		// or can we do this from our preferences page
	    		
	    		// if no points registered do nothing
	        	if (db!=null) {
	    			result = getAll();
	    			if (result != null) {
	    				android.util.Log.w("MapService","found geopoints");
	    				
	    				
	    				
	    			}else{
	    				android.util.Log.w("MapService","found no geopoints");
	    			}
	        	}else{
	        		android.util.Log.w("Map Service","db variable is null");
	        	}
	        	
	    	}else{
	    		android.util.Log.w("MapService","Map service is off");
	    		// but would need to switch back on the ringer if map service had previously switched off
	    		// check our db matrix for any live ringer shutdown and reverse

	    	}
			
			
		}

		public void onStatusChanged(String provider,int status,Bundle extras) {
			if( status==LocationProvider.AVAILABLE ){
				android.util.Log.w("MapService.LocationListener","Available");
			}else if( status==LocationProvider.TEMPORARILY_UNAVAILABLE) {
				android.util.Log.w("MapService.LocationListener","Temp Unavailable");
			}else {
				android.util.Log.w("MapService.LocationListener","Out of Service");
			}
		}
		
		public void onProviderEnabled(String s) {
			android.util.Log.w("MapService.LocationListener","Provider Enabled");
		}
		
		public void onProviderDisabled(String s) {
			android.util.Log.w("MapService.LocationListener","Provider Disabled");
		}
	};// end location listener
	
	
	public MapService() {
		super("MapService");//intentService version
		// keep thread alive by restarting if killed
		setIntentRedelivery(true);// restart if process dies before returning from onHandleIntent: restart process and redeliver intent
	}
	
	
	@Override
	// also need to check if mapping enabled before starting
	// to keep thread alive don't let this method return
	protected void onHandleIntent(Intent intent) {
		
		startDb();// we are on separate thread from main UI so this is safe
		
		// get a locationManager for the service
		locationManager = ((LocationManager) this.getSystemService(Context.LOCATION_SERVICE));
		
		// get provider based on requirements
		provider = getBestProvider(locationManager);
		
		// requestLocationUpdates there will be callback to location listener when there is an update
		requestLocationUpdates(locationListener,provider);// wrapper method around real requestLocationUpdates
		
		//List<String> providers = getBestProviders(locationManager);
		
		registerAllProximities();
			
		while(shouldRun==true);// keep thread alive in background
		
	}
	
	//Looks for an reasonable location provider
	private String getBestProvider(LocationManager locationManager) {
		

		Criteria criteria = getBestCriteria();	
		String provider = locationManager.getBestProvider(criteria, true);
		if(isProviderOK(provider)){
			return provider;
		}else{
			//try a provider with less stringent parameters
			criteria = getLesserCriteria();
			provider = locationManager.getBestProvider(criteria, true);
			if(isProviderOK(provider)){
				return provider;
			}else{
				criteria = getLeastCriteria();
				provider = locationManager.getBestProvider(criteria, true);
				if(isProviderOK(provider)){
					return provider;
				}
			}
		}
		android.util.Log.w("Map Service","No provider found");
		return "";
	}
	
	//Looks for accurate, no cost, low power providers
	private List<String> getBestProviders(LocationManager locationManager){
		Criteria criteria = getBestCriteria();
		
		List<String> allProviders = locationManager.getProviders(criteria, true);
		
		if (allProviders.isEmpty()){
			android.util.Log.w("MapService.getBestProviders","No providers available");
		}
		if (allProviders.size()==1 && isSameProvider(provider,allProviders.get(0))) {
			android.util.Log.w("MapService.getBestProviders","Only found same provider");
		}
		
		return allProviders;
	}
		
	private Criteria getBestCriteria(){
		Criteria criteria = new Criteria();
		criteria.setAccuracy(Criteria.ACCURACY_FINE);
		criteria.setAltitudeRequired(false);
		criteria.setBearingRequired(false);
		criteria.setCostAllowed(false);// need to ask customer
		criteria.setPowerRequirement(Criteria.POWER_LOW);
		
		return criteria;
	}
	
	private Criteria getLesserCriteria(){
		Criteria criteria = new Criteria();
		criteria.setAccuracy(Criteria.ACCURACY_FINE);
		criteria.setAltitudeRequired(false);
		criteria.setBearingRequired(false);
		criteria.setCostAllowed(false);// need to ask customer
		criteria.setPowerRequirement(Criteria.POWER_MEDIUM);
		
		return criteria;
	}
	
	private Criteria getLeastCriteria(){
		Criteria criteria = new Criteria();
		criteria.setAccuracy(Criteria.ACCURACY_COARSE);
		criteria.setAltitudeRequired(false);
		criteria.setBearingRequired(false);
		criteria.setCostAllowed(false);// need to ask customer
		criteria.setPowerRequirement(Criteria.NO_REQUIREMENT);
		
		return criteria;
	}
	
	public boolean isProviderOK(String provider) {
		if(provider !=null && !(provider.equals("")) ){
			return true;
		}
		return false;
	}
	
	private boolean isSameProvider(String provider1, String provider2) {
		if(provider1==null) {
			return provider2==null;
		}
		return provider1.equals(provider2);
	}
	
	/*
	 * Set up time and distance between location updates
	 * this needs to vary with distance from nearest zone
	 * when far low/no updates, when near increase frequency of updates
	 */
	public void requestLocationUpdates(LocationListener locationListener,String provider) {
			
		int minTime=4;
		int minDistance=100;
		
		if(locationManager != null){
			// remove old listener
			removeLocationUpdates(locationListener);
		}
		
		// note when screen off update only possible every 4 mins
		minTime = calcTimeBetweenUpdates();
		if(minTime < 1){
			minTime = ONE_MINUTE;// time between updates
		}
		
		minDistance = calcDistanceBetweenUpdates();
		if(minDistance < 1){
			minDistance = 100;// distance between updates(meters), less important to battery power
		}
		// set new update parameters
		locationManager.requestLocationUpdates(provider, minTime, minDistance, locationListener);
		
	}
	
	/*
	 * Remove old callbacks
	 */
	public void removeLocationUpdates(LocationListener locationListener) {
		
		try {
			locationManager.removeUpdates(locationListener);
		}catch(Exception e) {
			android.util.Log.w("Map Service","Can't remove update parameters");
		}
	}
	
	/*
	 * quick fix of cached data for provider(not necessarily currently enabled)
	 * but may be out of date so check age	
	 */
	public Location getLastLocation() {
	
		if( isProviderOK(provider) ){
			try{
				Location l = locationManager.getLastKnownLocation(provider);
				if( l == null){
					android.util.Log.w("Map Service","Last location is null");
				}else{
					android.util.Log.w("Map Server","Last location found");
				}
				return l;
			}catch(Exception e){
			}	
		}
		android.util.Log.w("Map Service","Cannot get last location");
		return null;
	}
	
	// is new location fix better than old location fix, is it even newer?
	public boolean isBetterLocation(Location newLocation, Location oldLocation) {
		if (oldLocation==null) {
			return true;
		}
		// check age of new location
		long timeDelta = newLocation.getTime() - oldLocation.getTime();
		boolean isMuchNewer = timeDelta > ONE_MINUTE;
		boolean isMuchOlder = timeDelta < -(ONE_MINUTE*2);
		boolean isNewer = timeDelta > 0;
		
		if(isMuchNewer) {
			return true;
		}
		if(isMuchOlder) {
			return false;
		}
		
		// check accuracy of new location
		int distanceDelta = (int) (newLocation.getAccuracy() - oldLocation.getAccuracy());
		boolean isLessAccurate = distanceDelta > 0;
		boolean isMoreAccurate = distanceDelta < 0;
		boolean isMuchLessAccurate = distanceDelta > 20;
		
		if (newLocation.getAccuracy() < 10) {
			// location stabilized remove coarse checking
			// could start with network and gps, then move to gps solely when it settles
			//lm.removeUpdates(lbounce);
			//lm.removeUpdates(lcoarse);
		}
		
		boolean isSameProvider = isSameProvider(newLocation.getProvider(), oldLocation.getProvider());
		
		if(isMoreAccurate) {
			return true;
		}else if(isNewer && !isLessAccurate) {
			return true;
		}else if(isNewer && !isMuchLessAccurate && isSameProvider) {
			return true;
		}
		
		return false;
		
	}
	

	
	/*
	 * Helper and Utility methods
	 * 
	 * 
	 */
	
	private boolean hasMonetaryCost(){
		return locationManager.getProvider(provider).hasMonetaryCost();
	}
	
	private long getAgeOfLocation(Location l){
		return l.getTime() - System.currentTimeMillis();		
	}
	
	// get accuracy of new location in meters
	private int getAccuracyOfLocation(Location l){
		return (int) (l.getAccuracy());
	}
	
	// register all our points in DB to fire intent if within radius
	private void registerAllProximities(){
		
		Intent intent = new Intent(this, Inner.class);
		PendingIntent pending = PendingIntent.getActivity(this, 0, intent, 0);
		// thread doesn't need to be alive to run pending intent!
		
		// go to db and get all points, loop through and register
		// create intent and pass through
		// only really need to register points within a certain radius
		// can re-register later
		if (db!=null) {
			result = getAll();
			if (result != null) {// there is a non-empty result
				while (result.moveToNext()) {
					android.util.Log.w("Map Service","found geopoints");
					String pk = result.getString(0);
					android.util.Log.w("Map Service",
							"pk: " + pk);// primary key is used to identify point
					String plat = String.valueOf( (Integer.parseInt(result.getString(1))/1E6) );
					android.util.Log.w("Map Service",
							"lat: " + plat);
					String plong = String.valueOf( (Integer.parseInt(result.getString(2))/1E6) );
					android.util.Log.w("Map Service",
							"long:" + plong);
					android.util.Log.w("Map Service",
							"radius: " + result.getString(3));
					
					GeoPoint point = new GeoPoint(Integer.parseInt(result.getString(1)),
							Integer.parseInt(result.getString(2)) );
					
					int radius = Integer.parseInt(result.getString(3));
					
					
				
					registerProximityIntent(point, radius, pending);
				}
					
			}else{
				android.util.Log.w("Map Service","no geopoints found");	
			}
			result.close();
		}	
	}
	
	// fires intent on entering(boolean extraKEY_PROXIMITY_ENTERING) or exiting
	private void registerProximityIntent(GeoPoint p, int r, PendingIntent i){
		
		
		// expiration -1 never expires
		locationManager.addProximityAlert(p.getLatitudeE6(),p.getLongitudeE6(), radius, -1, i);
	}
	
	// removeProximityAlert(PendingIntent intent);// since input is only intent will this remove all?
	
	
	private int calcNearestZone(Location bestEstimate) {
		// calculate distance to nearest zone
		return 0;
	}
	
	// vary logarithmically with distance to edge boundary and rate of movement(stabilization).
	private int calcTimeBetweenUpdates(){
		return ONE_MINUTE*4;
	}
	
	private int calcDistanceBetweenUpdates(){
		return 333;
	}
	
	// -1 represents speed unknown
	public float getSpeed() {
		float currentSpeed = -1;
		Location last = getLastLocation();
		if(last!=null){
			if(last.hasSpeed()) {
				currentSpeed=last.getSpeed();
			}
		}
		return currentSpeed;
	}
	
	
	
	// Use alarm manager to increase frequency of calling to less
	// than the default one minute
	// this would be used for example where we are on the boundary of a zone.
	// when screen is off we can only have location update once every four minutes, but
	// this doesn't stop us from checking location more often?
	public void setAlarm(int time){
		Intent intent = new Intent(this, Inner.class);
		PendingIntent pintent = PendingIntent.getService(this, 0, intent, 0);

		AlarmManager alarm = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
		if(time < 1){
			time = ONE_MINUTE;// default to 1 minute
		}
		alarm.set(AlarmManager.RTC_WAKEUP, time, pintent);
	}
		
	public void onDestroy() {
		super.onDestroy();
		// clean up thread
		android.util.Log.w("MapService","Destroying");
		stopDb();
	}
	
	protected void startDb() {
		try {
			db=(new Database(this).getWritableDatabase());// this=getContext()
			if(db==null) {
				throw new SQLiteException();
			}
			//db.execSQL("DELETE FROM geopoints"); // uncomment to clear table
			db.execSQL("CREATE TABLE IF NOT EXISTS geopoints (_id INTEGER PRIMARY KEY AUTOINCREMENT," +
					"latitude INTEGER NOT NULL, longitude INTEGER NOT NULL, radius INTEGER NOT NULL);");
			android.util.Log.w("MapService", "geopoints table started");
		} catch (SQLiteException e) {
			android.util.Log.e("DB error","Cannot open database");
		}
		if (db!=null){
			android.util.Log.w("MapService","Database opened");
		}else{
			android.util.Log.w("MapServie.startDb","db variable is null");
		}
		
	}
		
	private Cursor getAll() {
		Cursor tempCursor=db.rawQuery("SELECT * " +
					"FROM geopoints", null);
		return tempCursor;
	}
	
	protected void stopDb() {
		try {
			if(result!=null) {
				if(!result.isClosed()) {
					result.close();
				}	
				result=null;
			}
			if(db!=null) {
				if(db.isOpen()) {
					db.close();
					android.util.Log.w("Map Service","Database closed");
				}
				db=null;
				android.util.Log.w("Map Service","Database variable nullified");
			}
			
		}catch (SQLiteException e) {
			android.util.Log.e("Map Service","Cannot close database");
		}finally{
			try{
				if(db!=null){
					db.close();
					android.util.Log.w("Map Service","Database closed");
				}		
			}catch(SQLiteException e){
				android.util.Log.w("Map Service","Really can't close DB");
			}
		}
	}
	
	public class Inner extends IntentService{
		// IntentService should have its own thread

		public Inner(String name) {
			super(name);
			// TODO Auto-generated constructor stub
		}

		@Override
		protected void onHandleIntent(Intent intent) {
			// called by alarm manager or proximity alarm?
			boolean entering = false;
			boolean alarm = false;
			String e1 = null;
			Bundle b = intent.getBundleExtra("KEY_PROXIMITY_ENTERING");// "entering" is a boolean !
			if (b != null){
				e1 = b.getString("KEY_PROXIMITY_ENTERING");
			}
			if (e1.equals("true")){
				entering = true;
			}else{
				if (e1 == null){
					alarm = true;
				}
			}
			if (!alarm){
				android.util.Log.w("Map Service","Entering/Exiting zone");
			}else{
				android.util.Log.w("Map Service","Alarm triggerd");
			}
			
			
		}
		
	}
}
